/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.support.scripting;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.script.Bindings;

import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.primitives.cadmium.Cadmium;
import fr.x9c.cadmium.support.Helper;
import fr.x9c.cadmium.support.Helper;

/**
 * Bindings from strings to OCaml values.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.1
 * @since 1.1
 */
public final class OCamlBindings implements Bindings {

    /** Actual bindings. */
    private final Map<String, Object> values;

    /**
     * Creates empty bindings.
     */
    public OCamlBindings() {
        this.values = new HashMap<String, Object>();
    } // end empty constructor

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean containsKey(final Object key) {
        assert key != null : "null key";
        if (((String) key).length() == 0) {
            throw new IllegalArgumentException("invalid key");
        } // end if
        return this.values.containsKey(key);
    } // end method 'containsKey(Object)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object get(final Object key) {
        assert key != null : "null key";
        if (((String) key).length() == 0) {
            throw new IllegalArgumentException("invalid key");
        } // end if
        return this.values.get(key);
    } // end method 'get(Object)'

    /**
     * Returns the value to which the underlying map maps the specified key.
     * Some conversion may occur in order to return a <tt>Value</tt>
     * instance: all primitive wrappers as well as string are converted.
     * Other values are returned as elements whose type is
     * <i>Cadmium.java_object</i>.
     * @param key key whose associated value is to be returned
     * @return the value to which the underlying map maps the specified key
     * @throws NullPointerException if key is null
     * @throws ClassCastException if key is not a string
     * @throws IllegalArgumentException if key is an empty string
     */
    Value getValue(final Object key)
        throws NullPointerException, ClassCastException,
               IllegalArgumentException {
        assert key != null : "null key";
        if (containsKey(key)) {
            return convert(get(key));
        } else {
            return Cadmium.createObject(null);
        } // end if/else
    } // end method 'getValue(Object)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object put(final String name, final Object value) {
        assert name != null : "null name";
        if (name.length() == 0) {
            throw new IllegalArgumentException("invalid key");
        } // end if
        return this.values.put(name, value);
    } // end method 'put(String, Object)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void putAll(final Map<? extends String, ? extends Object> toMerge) {
        assert toMerge != null : "null toMerge";
        for (Map.Entry<String, Object> e : this.values.entrySet()) {
            put(e.getKey(), e.getValue());
        } // end for
    } // end method 'putAll(Map<? extends String, ? extends Object>'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object remove(final Object key) {
        assert key != null : "null key";
        if (((String) key).length() == 0) {
            throw new IllegalArgumentException("invalid key");
        } // end if
        return this.values.remove(key);
    } // end method 'remove(Object)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void clear() {
        this.values.clear();
    } // end method 'clear()'

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean containsValue(final Object value) {
        return this.values.containsValue(value);
    } // end method 'containsValue(Object)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<Map.Entry<String, Object>> entrySet() {
        return this.values.entrySet();
    } // end method 'entrySet()'

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEmpty() {
        return this.values.isEmpty();
    } // end method 'isEmpty()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<String> keySet() {
        return this.values.keySet();
    } // end method 'keySet()'

    /**
     * {@inheritDoc}
     */
    @Override
    public int size() {
        return this.values.size();
    } // end method 'size()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<Object> values() {
        return this.values.values();
    } // end method 'values()'

    /**
     * Returns an instance that is either the passed parameter if it is an
     * instance of <tt>OCamlBindings</tt>, or a new instance otherwise.
     * @param b bindings - should not be <tt>null</tt>
     * @return an <tt>OCamlBindings</tt> instance of the passed bindings
     */
    static OCamlBindings instance(final Bindings b) {
        assert b != null : "null b";
        if (b instanceof OCamlBindings) {
            return (OCamlBindings) b;
        } else {
            final OCamlBindings res = new OCamlBindings();
            res.putAll(b);
            return res;
        } // end if/else
    } // end method 'instance(Bindings)'

    /**
     * <i>Converts</i> a Java value into is Objective Caml equivalent.
     * @param v value to convert
     * @return converted value
     */
    static Value convert(final Object v) {
        if (v == null) {
            return Cadmium.createObject(null);
        } // end if
        final Class c = v.getClass();
        if (Value.class.equals(c)) {
            return (Value) v;
        } else if (Block.class.equals(c)) {
            return Value.createFromBlock((Block) v);
        } else if (Boolean.class.equals(c)) {
            return ((Boolean) v).booleanValue()
                ? Value.TRUE
                : Value.FALSE;
        } else if (Byte.class.equals(c)) {
            return Value.createFromLong(((Byte) v).byteValue());
        } else if (Character.class.equals(c)) {
            return Value.createFromLong(((Character) v).charValue());
        } else if (Double.class.equals(c)) {
            return Helper.createFloat(((Double) v).doubleValue());
        } else if (Float.class.equals(c)) {
            return Helper.createFloat(((Float) v).floatValue());
        } else if (Integer.class.equals(c)) {
            return Helper.createInt32(((Integer) v).intValue());
        } else if (Long.class.equals(c)) {
            return Helper.createInt64(((Long) v).longValue());
        } else if (Short.class.equals(c)) {
            return Value.createFromLong(((Short) v).shortValue());
        } else if (String.class.equals(c)) {
            return Helper.createString((String) v);
        } else {
            return Cadmium.createObject(v);
        } // end if/elsif/else
    } // end method 'convert(Object)'

} // end class 'OCamlBindings'
